#include "./BSP/RTC/bsp_rtc.h"
#include <stdio.h>

/* -------------------------------------------- 全局变量定义 -------------------------------------------- */

RTC_HandleTypeDef g_rtc_handle; /* RTC控制句柄 */
_calendar_obj calendar;         /* 时间结构体，保存当前时间日期信息 */

/**
 * @brief       RTC写入后备区域SRAM
 * @details     STM32F103提供42个16位后备寄存器，总共84字节的非易失性存储空间。
 *              这些寄存器在主电源断电时由备用电池(VBAT)供电，数据不会丢失。
 *              常用于保存系统配置参数、校准数据等重要信息。
 * @param       bkrx : 后备区寄存器编号,范围:0~41
 *                     对应 RTC_BKP_DR1~RTC_BKP_DR42
 * @param       data : 要写入的数据,16位长度
 * @retval      无
 */
void rtc_write_bkr(uint32_t bkrx, uint16_t data)
{
    HAL_PWR_EnableBkUpAccess(); /* 取消备份区写保护 */
    HAL_RTCEx_BKUPWrite(&g_rtc_handle, bkrx + 1, data);
}

/**
 * @brief       RTC读取后备区域SRAM
 * @details     从指定的后备寄存器中读取16位数据。后备寄存器的数据在系统
 *              复位、软件复位时保持不变，只有在备用电池电量耗尽或执行
 *              备份域复位时才会丢失。
 * @param       bkrx : 后备区寄存器编号,范围:0~41
 *                     对应 RTC_BKP_DR1~RTC_BKP_DR42
 * @retval      读取到的16位数据值
 * @note        读取操作不需要解除写保护
 */
uint16_t rtc_read_bkr(uint32_t bkrx)
{
    uint32_t temp = 0;
    temp = HAL_RTCEx_BKUPRead(&g_rtc_handle, bkrx + 1);
    return (uint16_t)temp;  /* 返回读取到的值 */
}

/**
 * @brief       RTC初始化
 * @note        默认尝试使用LSE,当LSE启动失败后,切换为LSI.
 *              通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:
 *              当BKP0==0X5050时,使用的是LSE
 *              当BKP0==0X5051时,使用的是LSI
 *              注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.
 * @param       无
 * @retval      0,成功
 *              1,进入初始化模式失败
 */
uint8_t rtc_init(void)
{
    /* 检查是否为首次配置RTC时钟 */
    uint16_t bkpflag = 0;
    
    /* 使能相关时钟和解除写保护 */
    __HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源管理时钟 */
    __HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份域时钟 */
    HAL_PWR_EnableBkUpAccess(); /* 取消备份域写保护，允许访问RTC和后备寄存器 */

    bkpflag = rtc_read_bkr(0);  /* 读取BKP0标志位，判断时钟源类型 */
    
    /* 配置RTC基本参数 */
    g_rtc_handle.Instance = RTC;
    //g_rtc_handle.Init.AsynchPrediv = 32767; /* 异步预分频器：32768-1=32767，产生1Hz时钟，这里也可以用 RTC_AUTO_1_SECOND */
    g_rtc_handle.Init.AsynchPrediv = RTC_AUTO_1_SECOND; /* 自动根据时钟源计算预分频器 */
    g_rtc_handle.Init.OutPut = RTC_OUTPUTSOURCE_NONE;   /* 不输出RTC信号到外部引脚 */
    
    // /* 复位备份域以清除之前配置 */
    // RCC->BDCR |= 1 << 16;       /* 置位BDRST，开始备份域软复位 */
    // RCC->BDCR &= ~(1 << 16);    /* 清零BDRST，结束备份域软复位 */
    /* 不强制复位备份域，保持与CubeMX生成代码一致的行为 */
    
    /* 初始化RTC外设 */
    if (HAL_RTC_Init(&g_rtc_handle) != HAL_OK)
    {
        return 1; /* RTC初始化失败 */
    }

    /* 检查是否需要设置默认时间（之前未初始化过，重新配置） */
    if ((bkpflag != 0X5050) && (bkpflag != 0x5051)) /* 0x5050=LSE, 0x5051=LSI */
    {
        rtc_set_time(2025, 8, 14, 10, 20, 00); /* 设置默认时间：2025年8月14日 10:20:00 */
    }

    /* 配置RTC中断 */
    __HAL_RTC_ALARM_ENABLE_IT(&g_rtc_handle, RTC_IT_SEC); /* 使能秒中断 */
    HAL_NVIC_SetPriority(RTC_IRQn, 0x2, 0); /* 设置中断优先级：抢占优先级2，子优先级0 */
    HAL_NVIC_EnableIRQ(RTC_IRQn);           /* 使能RTC中断向量 */

    rtc_get_time(); /* 读取当前时间并更新calendar结构体 */
    
    return 0; /* 初始化成功 */
}

/**
 * @brief       RTC底层初始化
 * @note        RTC底层驱动，时钟配置,此函数会被HAL_RTC_Init()调用
 *              自动检测LSE时钟源可用性，失败时切换到LSI
 * @param       hrtc : RTC句柄
 * @retval      无
 */
void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
    uint16_t retry = 200; /* LSE起振超时计数器，最大等待1秒 */
    
    __HAL_RCC_RTC_ENABLE(); /* 使能RTC外设时钟 */

    RCC_OscInitTypeDef rcc_oscinitstruct;
    RCC_PeriphCLKInitTypeDef rcc_periphclkinitstruct;
    
    /* 方案一：优先尝试使用外部低速晶振LSE（32.768KHz） */
    /* LSE具有更高的时间精度，温度稳定性好，适合RTC应用 */
    RCC->BDCR |= 1 << 0; /* 置位LSEON，启动外部低速振荡器LSE */
    
    /* 等待LSE稳定，检查LSERDY位 */
    while (retry && ((RCC->BDCR & 0X02) == 0)) /* 检查LSERDY位（bit1） */
    {
        retry--;
        HAL_Delay(5); /* 每5ms检查一次，总计等待1秒 */
    }
    
    if (retry == 0) /* LSE起振失败，切换到备用方案LSI */
    {
        /* 方案二：使用内部低速振荡器LSI（约40KHz） */
        /* LSI精度较低但无需外部晶振，作为LSE的备用时钟源 */
        rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;  /* 配置LSI振荡器 */
        rcc_oscinitstruct.LSEState = RCC_LSI_ON;                    /* 启动LSI */
        rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE;              /* 不使用PLL */
        HAL_RCC_OscConfig(&rcc_oscinitstruct);                      /* 应用振荡器配置 */

        /* 配置RTC时钟源为LSI */
        rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;   /* 选择要配置的外设 RTC */
        rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;   /* RTC时钟源选择 LSI */
        HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct);                /* 配置设置的rcc_periphClkInitStruct */
        rtc_write_bkr(0, 0X5051); /* 在BKP0写入LSI标志 */
    }
    else /* LSE起振成功，使用高精度外部晶振 */
    {
        /* 配置LSE为RTC时钟源 */
        rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;  /* 选择要配置的振荡器 */
        rcc_oscinitstruct.LSEState = RCC_LSE_ON;                    /* LSE状态：开启 */
        rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE;              /* 不使用PLL */
        HAL_RCC_OscConfig(&rcc_oscinitstruct);                      /* 应用振荡器配置 */
        
        /* 配置RTC时钟源为LSE */
        rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;   /* 选择要配置外设 RTC */
        rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;   /* RTC时钟源选择LSE */
        HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct);                /* 配置设置的rcc_periphClkInitStruct */
        rtc_write_bkr(0, 0X5050);                                           /* 在BKP0写入LSE标志 */
    }
}

/**
 * @brief       RTC时钟中断服务函数
 * @note        秒钟中断服务函数,顺带处理闹钟标志
 *              根据RTC_CRL寄存器的 SECF 和 ALRF 位区分是哪个中断
 * @param       无
 * @retval      无
 */
void RTC_IRQHandler(void)
{
    /* 处理秒中断：每秒触发一次，用于实时更新时间 */
    if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_SEC) != RESET)     /* 秒中断 */
    {
        rtc_get_time();                                                     /* 更新时间 */
        __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_SEC);            /* 清除秒中断 */
        //printf("sec:%d\r\n", calendar.sec);                               /* 调试用：打印当前秒数 */
    }

    /* 处理闹钟中断：闹钟时间到达时触发 */
    if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF) != RESET)   /* 闹钟标志 */
    {
        __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF);          /* 清除闹钟标志 */    
        printf("Alarm Time:%d-%d-%d %d:%d:%d\n", calendar.year, calendar.month, calendar.date, 
                    calendar.hour, calendar.min, calendar.sec);             /* 打印当前时间 */
    }

    /* 清除溢出中断标志：防止计数器溢出导致的异常 */
    __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_OW);                 /* 清除溢出中断标志 */
    
    /* 等待RTC寄存器操作完成：确保所有写操作都已生效 */
    while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF));       /* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
}

/**
 * @brief       判断年份是否是闰年
 * @note        月份天数表:
 *              月份   1  2  3  4  5  6  7  8  9  10 11 12
 *              闰年   31 29 31 30 31 30 31 31 30 31 30 31
 *              非闰年 31 28 31 30 31 30 31 31 30 31 30 31
 *              闰年规则: 四年闰百年不闰，四百年又闰
 * @param       year : 年份
 * @retval      0, 非闰年; 1, 是闰年
 */
static uint8_t rtc_is_leap_year(uint16_t year)
{
    /* 闰年规则: 四年闰百年不闰，四百年又闰 */
    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

/**
 * @brief       设置时间, 包括年月日时分秒
 *   @note      以1970年1月1日为基准, 往后累加时间
 *              合法年份范围为: 1970 ~ 2105年
 *              HAL默认为年份起点为2000年
 * @param       syear : 年份
 * @param       smon  : 月份
 * @param       sday  : 日期
 * @param       hour  : 小时
 * @param       min   : 分钟
 * @param       sec   : 秒钟
 * @retval      0, 成功; 1, 失败;
 */
uint8_t rtc_set_time(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
    uint32_t seccount = 0;

    /* 第一步：将日期时间转换为Unix时间戳（从1970年开始的秒数） */
    seccount = rtc_date2sec(syear, smon, sday, hour, min, sec);

    /* 第二步：使能时钟和解除写保护（RTC寄存器写操作的必要步骤） */
    __HAL_RCC_PWR_CLK_ENABLE();     /* 使能电源管理时钟 */
    __HAL_RCC_BKP_CLK_ENABLE();     /* 使能备份域时钟 */
    HAL_PWR_EnableBkUpAccess();     /* 取消备份域写保护 */
    
    /* 第三步：进入RTC配置模式 */
    RTC->CRL |= 1 << 4;             /* 置位CNF位，允许对RTC计数器进行写操作 */
    
    /* 第四步：将时间戳写入RTC计数器（32位分为高低16位） */
    RTC->CNTL = seccount & 0xffff;  /* 写入低16位 */
    RTC->CNTH = seccount >> 16;     /* 写入高16位 */
    
    /* 第五步：退出RTC配置模式 */
    RTC->CRL &= ~(1 << 4);          /* 清零CNF位，结束配置模式 */

    /* 第六步：等待RTC寄存器操作完成 */
    while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF)); /* 等待RTOFF=1 */

    return 0; /* 设置成功 */
}

/**
 * @brief       设置闹钟, 具体到年月日时分秒
 * @note        以1970年1月1日为基准, 往后累加时间
 *              合法年份范围为: 1970 ~ 2105年
 *              闹钟触发时会产生中断，在RTC_IRQHandler中处理
 * @param       syear : 年份
 * @param       smon  : 月份
 * @param       sday  : 日期
 * @param       hour  : 小时
 * @param       min   : 分钟
 * @param       sec   : 秒钟
 * @retval      0, 成功; 1, 失败
 */
uint8_t rtc_set_alarm(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
    uint32_t seccount = 0;

    /* 将闹钟日期时间转换为Unix时间戳 */
    seccount = rtc_date2sec(syear, smon, sday, hour, min, sec); /* 将年月日时分秒转换成总秒钟数 */

    /* 使能时钟和解除写保护（闹钟寄存器写操作的必要步骤） */
    __HAL_RCC_PWR_CLK_ENABLE();     /* 使能电源管理时钟 */
    __HAL_RCC_BKP_CLK_ENABLE();     /* 使能备份域时钟 */
    HAL_PWR_EnableBkUpAccess();     /* 取消备份域写保护 */
    
    /* 进入RTC配置模式 */
    RTC->CRL |= 1 << 4;             /* 置位CNF位，允许对RTC闹钟寄存器进行写操作 */
    
    /* 将闹钟时间戳写入RTC闹钟寄存器 */
    RTC->ALRL = seccount & 0xffff;  /* 写入闹钟低16位 */
    RTC->ALRH = seccount >> 16;     /* 写入闹钟高16位 */
    
    /* 退出RTC配置模式 */
    RTC->CRL &= ~(1 << 4);          /* 清零CNF位，结束配置模式 */

    /* 等待RTC寄存器操作完成 */
    while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF)); /* 等待RTOFF=1 */

    return 0; /* 闹钟设置成功 */
}

/**
 * @brief       得到当前的时间
 * @note        该函数不直接返回时间, 时间数据保存在calendar结构体里面
 *              从RTC计数器读取秒钟数，转换为年月日时分秒格式
 * @param       无
 * @retval      无
 */
void rtc_get_time(void)
{
    static uint16_t daycnt = 0; /* 静态变量：保存上次计算的天数，用于优化性能 */
    uint32_t seccount = 0;      /* Unix时间戳：从1970年1月1日开始的秒数 */
    uint32_t temp = 0;          /* 临时变量：用于各种计算 */
    uint16_t temp1 = 0;         /* 临时变量：用于年份和月份计算 */
    /* 平年每月天数表：1-12月对应的天数 */
    const uint8_t month_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    /* 第一步：从RTC计数器读取当前时间戳（32位分为高低16位） */
    seccount = RTC->CNTH;       /* 读取高16位 */
    seccount <<= 16;            /* 左移16位 */
    seccount += RTC->CNTL;      /* 加上低16位，得到完整的32位时间戳 */

    /* 第二步：计算当前是第几天（1天=86400秒） */
    temp = seccount / 86400;

    /* 第三步：只有当天数发生变化时才重新计算年月日（性能优化） */
    if (daycnt != temp)
    {
        daycnt = temp;          /* 更新天数记录 */
        temp1 = 1970;           /* 从Unix纪元年份1970开始计算 */

        /* 第四步：计算年份（考虑闰年因素） */
        while (temp >= 365)     /* 剩余天数超过一年 */
        {
            if (rtc_is_leap_year(temp1)) /* 判断是否为闰年 */
            {
                if (temp >= 366) /* 闰年有366天 */
                {
                    temp -= 366; /* 减去闰年的天数 */
                }
                else
                {
                    break;       /* 不够一个闰年，退出循环 */
                }
            }
            else
            {
                temp -= 365;     /* 减去平年的天数 */
            }
            temp1++;             /* 年份加1 */
        }
        calendar.year = temp1;   /* 得到最终年份 */

        /* 第五步：计算月份（考虑闰年2月的特殊情况） */
        temp1 = 0;               /* 重置temp1作为月份索引（0-11对应1-12月） */
        while (temp >= 28)       /* 剩余天数超过一个月（最小月份28天） */
        {
            /* 特殊处理：闰年2月有29天 */
            if (rtc_is_leap_year(calendar.year) && temp1 == 1) /* temp1==1对应2月 */
            {
                if (temp >= 29)  /* 闰年2月有29天 */
                {
                    temp -= 29;  /* 减去闰年2月的天数 */
                }
                else
                {
                    break;       /* 不够一个闰年2月，退出循环 */
                }
            }
            else /* 平年或非2月 */
            {
                if (temp >= month_table[temp1]) /* 检查是否超过当前月的天数 */
                {
                    temp -= month_table[temp1]; /* 减去当前月的天数 */
                }
                else
                {
                    break; /* 不够一个月，退出循环 */
                }
            }
            temp1++; /* 月份索引加1 */
        }
        calendar.month = temp1 + 1; /* 转换为1-12月 */
        calendar.date = temp + 1;   /* 剩余天数+1就是当前日期 */
    }

    /* 第六步：计算当日的时分秒（每次都要计算） */
    temp = seccount % 86400;            /* 取余数得到当日的秒数 */
    calendar.hour = temp / 3600;        /* 1小时=3600秒 */
    calendar.min = (temp % 3600) / 60;  /* 1分钟=60秒 */
    calendar.sec = (temp % 3600) % 60;  /* 剩余秒数 */
    
    /* 第七步：计算星期几（使用基姆拉尔森公式） */
    calendar.week = rtc_get_week(calendar.year, calendar.month, calendar.date);
}

/**
 * @brief       根据年月日计算星期几
 * @note        输入公历日期得到星期(起始时间为: 公元0年3月1日开始, 输入往后的任何日期, 都可以获取正确的星期)
 *              使用 基姆拉尔森计算公式 计算, 原理说明见此贴:
 *              https://www.cnblogs.com/fengbohello/p/3264300.html
 * @param       year  : 年份
 * @param       month : 月份
 * @param       day   : 日期
 * @retval      0, 星期天; 1 ~ 6: 星期一 ~ 星期六
 */
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day)
{
    uint8_t week = 0;

    /* 基姆拉尔森公式的特殊处理：将1月2月看作上一年的13月14月 */
    if (month < 3) /* 1月2月需要特殊处理 */
    {
        month += 12; /* 1月变成13月，2月变成14月 */
        --year;      /* 年份减1，即归属于上一年 */
    }

    /* 基姆拉尔森公式计算星期： */
    /* W = (d + 1 + 2*m + 3*(m+1)/5 + y + y/4 - y/100 + y/400) mod 7 */
    /* 其中：d=日期, m=月份, y=年份 */
    week = (day + 1 + 2 * month + 3 * (month + 1) / 5 + year + (year >> 2) - year / 100 + year / 400) % 7;
    
    return week; /* 返回值：0=星期天, 1-6=星期一至星期六 */
}

/**
 * @brief       将年月日时分秒转换成秒钟数
 * @note        以1970年1月1日为基准, 1970年1月1日, 0时0分0秒, 表示第0秒钟
 *              最大表示到2105年, 因为uint32_t最大表示136年的秒钟数(不包括闰年)!
 *              本代码参考linux mktime函数, 原理说明见此贴:
 *              http://www.openedv.com/thread-63389-1-1.html
 * @param       syear : 年份
 * @param       smon  : 月份
 * @param       sday  : 日期
 * @param       hour  : 小时
 * @param       min   : 分钟
 * @param       sec   : 秒钟
 * @retval      转换后的秒钟数
 */
static long rtc_date2sec(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
    uint32_t Y, M, D, X, T; /* 中间计算变量：Y=年份天数, M=月份天数, D=日期天数, X=总天数, T=总秒数 */
    signed char monx = smon;/* 将月份转换为带符号类型，方便后续算法处理 */

    /* 第一步：月份调整算法（Linux mktime算法的核心思想） */
    /* 将2月放在最后处理，这样闰年的额外一天就在年末，简化计算 */
    if (0 >= (monx -= 2)) /* 将月份减2：3-12月变成1-10，1-2月变成负数 */
    {
        monx += 12;      /* 1月变成11月，2月变成12月（上一年的） */
        syear -= 1;      /* 年份减1，因为1-2月归属于上一年 */
    }

    /* 第二步：计算从公元元年到指定年份的总天数 */
    /* 公式：(year-1)*365 + 闰年数 */
    /* 闰年数 = year/4 - year/100 + year/400（格里高利历闰年规则） */
    Y = (syear - 1) * 365 + syear / 4 - syear / 100 + syear / 400;
    
    /* 第三步：计算月份对应的天数（使用近似公式） */
    /* 公式：367*month/12 - 30 + 59 */
    /* 这个公式是经验公式，能够准确计算每个月的累计天数 */
    M = 367 * monx / 12 - 30 + 59;
    
    /* 第四步：日期天数（减1因为日期从1开始计数） */
    D = sday - 1;
    
    /* 第五步：计算从1970年1月1日开始的天数 */
    /* 719162是公元元年到1970年1月1日的天数（预计算常数） */
    X = Y + M + D - 719162;
    
    /* 第六步：将天数转换为秒数，并加上时分秒 */
    /* 计算顺序：天->24小时->分钟->秒钟 */
    T = ((X * 24 + hour) * 60 + min) * 60 + sec;
    
    return T; /* 返回Unix时间戳（从1970年1月1日开始的秒数） */
}
